/*jshint strict:false 
*/
/*

miketodo.js extension for PSPad
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.


#!todofile!#

Mike's todo helper scripts for PSPad: miketodo.js v1.0
www.mertsock.com
GNU GPL license, see the script file for information.

Please read the rest of this file for instructions. After reading the instructions, try running the Daily Rotate and Weekly Rotate items in the Scripts menu to see how this works.

If you installed the miketodo.js file while PSPad is running, you will need to run the Recompile Scripts command in the Scripts menu.

Who this is for:
* If you like outlining, you'll like this method of tracking to-do items
* If you don't want to give up control of your to-do list to some database, like most todo-tracking programs require, you'll like this

History:
* I don't like the todo-tracking programs out there that require you to store your notes in a database. I like plain text files, and the opportunity to combine any set of tools to do what you want with the files.
* So I developed a rather specialized system for tracking programming projects, etc. The status of each to-do item would be identified by symbols like - for "not started" and + for "completed".
* As a user of PSPad, I decided to write some scripts to help streamline maintaining these to-do lists, and allow for a consistent to keep track of past completed items. Thus this extension.
* To make use of this PSPad extension, you'll need to keep track of your to-do list in a text file, like this readme file. And you'll have to learn a bit about how the extension interprets your to-do items. So please read the Basic usage and File Format sections below. It's really easy.

Basic usage:
1. Create and organize your to-do list file in a format similar to this file. See "File Format" below for more info.
2. First thing each morning, use the "Daily Rotate" script to log completed items, update item priority, etc. Use "Weekly Rotate" INSTEAD OF "Daily Rotate" on the first day of the week. These script commands modify which ever file is currently being displayed by PSPad.
3. Use the View Log command to see a nice log of previously completed items.
4. If you want to set up keyboard shortcuts, run the View Script command in the Scripts menu to edit the miketodo.js file, and then take a look at the Init() function at the end of the file.

Bugs/limitations:
* Won't work reliably for new files not yet saved to disk
* Multi-level lists (indenting) and multi-line list items not yet supported
* I wrote some nice regular expressions to more flexibly handle the format of section identifiers and todo items, but not all of them work so currently some less flexible string comparisons are being used

File Format:

* A todo file is identified by the "todofile" magic string on the first line. Without this, the todo scripts will refuse to run, in order to prevent unwanted changes to non-todo files.

* The todo list is like a bulleted list of items. The "bullet" character used determines the status of the item:
  - item is not started
  > item is in progress
  + item is complete
  x this item is cancelled
  * This is a note (it has no status and is never automatically removed/logged)
  ! any other character is treated like a note as well.

* The todo file is broken into "sections". Each section may be "freeform", or a specific section type that has various rules for Daily/Weekly Rotate.

* Sections are identified by a # symbol followed by the section name. All todo items following that line are part of the section. You're not required to use every section listed below, and the order doesn't matter. The recognized sections are:
  * in queue
      Suggested uses: To quickly jot down new todo items. Then either mark them
      complete right away, if they're quick, or prioritize them later by moving
      to the #today section or #next week, etc.
  * to publish
      Useful for a temporary list of files to edit/transfer, people to call, etc
  * today
      Things you want to complete today.
  * tomorrow
      Things you want to complete tomorrow.
  * this week
      Things you want to complete this week.
  * next week
      Things you want to complete next week.
  * other week
  *   Things you want to complete in the near future  
  * eventually
      Low-priority items you'll deal with later.

* Anything outside of a recognized section (for example, this line) is a "freeform" section, and left untouched by the todo scripts. Useful for other general notes you want to have in the todo file. To explicitly end a recognized section and start a freeform section (see end of this file), write a line that only has these two characters: #/

* Behavior of sections is as follows:
  * Completed/cancelled items in "in queue" and "to publish" are simply removed and not logged. In all other (non-freeform) sections, they are removed from the todo file and added to the log file.
  # Completed/cancelled items in "in queue" and "to publish" are logged!!   
  * Incomplete items in the "tomorrow" section are moved to the "today" section.
  * For Weekly Rotate, incomplete items in the "next week" section are moved to the "this week" section.

END OF INSTTRUCTIONS. BELOW ARE SAMPLE TODO ITEMS




#in queue
x add Kitchen-Sink extension to PSPad
- prioritize bug on web site
- what did that guy ask me about Slashdot?

#to publish
- index.html
> admin/index.php
+ images/cow.jpg
- images/horse.jpg

#today

- Run the "Daily Rotate" command in the Scripts menu
- then run the "View Log" command in the Scripts menu
* This is a note. Notes can be anywhere, as long as the first character in the line is not a - or + or > or x (see "File Format" above)
> write readme file for miketodo.js
+ fix some final bugs in miketodo.js
- make a backup 

#tomorrow

- calendar updates
+ call John Doe (he called me)

#this week
> upload miketodo.js to PSPad site and personal site

#next week
- start work on next enhancements to todo scripts
> shop for gifts

#other week
- 20100903  Enhance todo script with #other week

#eventually
- buy a new car
> experiment with Windows Scripting Host

#/

The line above ended the "eventually" section, so this area here is a freeform section.
- Note that the todo item markers like "-" and "+" are ignored here.
- I find it useful to use this area for general notes, ideas for new projects, etc.


*/
var module_name = "MikesTodos";
var module_ver = "1.1";

///////////////// Configuration ///////////////////////////

var config = {
  // will quit if this isn't found at top of file
  magicHeader: "#!todofile!#",
  // this plus a section label starts a section area (and ends prev section)
  // plain string instead of RegExp b/c it's concatenated with stuff
  labelPrefixRegex: "^#+ *",
  labelSuffixRegex: " *$",
  // when this is encountered, ends a section and starts freeform area
  endSectionRegex: new RegExp("^#+/$"),
  // directory locations
  todoLogExt: ".log.txt",
  backupExt: ".bak.txt",
  newline: "\r\n",
  makeBackup: true,
  doLogging: true,
  modifyText: true,
  sections: {
    // SECTIONS: each section object is a todo list section.
    //   label is the text identifying each section in the file
    //   logCompleted is true if move completed items to logfile
    //   xxxRotate if not null, label of section to move items to
    // items yet to be prioritized
    inbox: {
      label: "in queue",
      logCompleted: true,
      dailyRotate: null,
      weeklyRotate: null
    },
    // rotated items
    today: {
      label: "today",
      logCompleted: true,
      dailyRotate: null,
      weeklyRotate: null
    },
    tomorrow: {
      label: "tomorrow",
      logCompleted: true,
      dailyRotate: "today",
      weeklyRotate: null
    },
    thisWeek: {
      label: "this week",
      logCompleted: true,
      dailyRotate: null,
      weeklyRotate: null
    },
    nextWeek: {
      label: "next week",
      logCompleted: true,
      dailyRotate: null,
      weeklyRotate: "this week"
    },
    // not auto-rotated
    eventually: {
      label: "eventually",
      logCompleted: true,
      dailyRotate: null,
      weeklyRotate: null
    },
    topub: {
      label: "to publish",
      logCompleted: true,
      dailyRotate: null,
      weeklyRotate: null
    }
    // freeform section is not touched by scripts
    //freeformLbl: "freeform section"
  } // end of sections sub-obj
};

function copyFile(src, dest)
{
  // http://msdn.microsoft.com/en-us/library/z9ty6h50%28VS.85%29.aspx
  var fso = new ActiveXObject("Scripting.FileSystemObject");
  fso.CopyFile(src, dest);
}

//plain trim function wim
function trim(text)
{
  return text.replace(/^\s+|\s+$/g, '');
}


// trim extension from filename
function trimExt(filename)
{
  var dotIdx = filename.lastIndexOf(".");
  return filename.substr(0, dotIdx > 0 ? dotIdx : filename.length);
}

// for trimming elements of a given value from end of array
// returns target length of new array
function rTrimIndex(ary, value) {
  for (var i = ary.length - 1; i >= 0; --i)
    if (ary[i] !== value) return i + 1;
  return 0;
}

function makeBackup(ed)
{
  try {
    var origFile = ed.fileName(),
        bakFile = trimExt(origFile) + config.backupExt;
    copyFile(origFile, bakFile);
    //echo("copyFile(" + origFile + ", " + bakFile + ");");
  } catch(e) {
    echo(".fileName failed");
  }
}

function getAndVerifyAndBackupCurrentFile() 
{
	var ed = newEditor();
	ed.assignActiveEditor();
	
	// now verify it's the right file type
	var origText = ed.text();
	if ( 0 != origText.indexOf(config.magicHeader) )
	  throw "File is not a TODO file.";
	
	// all ok, do backup etc.
	if ( config.makeBackup )
  	makeBackup(ed);
	
	return ed;
}



/*==================================================================================================
  Description   Returns the filename portion of the supplied full path filename

  20110503      Wim       Created
==================================================================================================*/
function extractFileName(fileName)
{
  var results = fileName.split('\\');
  return results[results.length - 1];
}


/*==================================================================================================
  Description   Returns the filepath portion of the supplied full path filename

  20110503      Wim       Created
==================================================================================================*/
function extractFilePath(fileName)
{
  var results = fileName.split('\\');
  return results.slice(0, -1).join('\\');
}


// enum
var TodoType = {
  notStarted: "not_started",
  inProgress: "in_progress",
  complete: "complete",
  remark: "remark"
};

var TTRgx = {
  notStarted: /^ *-/,
  inProgress: /^ *>/,
  complete:   /^ *[+x]/
};

function checkTodoType(line) {
  if ( TTRgx.notStarted.test(line) )
    return TodoType.notStarted;
  if ( TTRgx.inProgress.test(line) )
    return TodoType.inProgress;
  if ( TTRgx.complete.test(line) )
    return TodoType.complete;
  return TodoType.remark;
};


//////////////////////////////// Todo class ////////////////////////////
function displaySections(msg, sections)
{
  logAddLine(msg + ' >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');
  logAddLine(sections.length);
  for ( var ss in sections ) 
  {
    if (Object.prototype.hasOwnProperty.call(sections, ss))
    {
      var sec = sections[ss];
      var lbl = sec.cfg ? sec.cfg.label : "*"; 
      var desc = lbl + "/" + sec.startLine + "/" + sec.endLine;
      logAddLine(desc);
    }
  }
  logAddLine(msg + ' <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<');
}

function Todo(ed)
{
  if ( !ed ) throw "Failed to init.";
  // editor object for current file
  this.ed = ed;
  // original unmodified text
  this.origText = ed.text();
  this.origLines = this.origText.split(config.newline);
  // this.sections[labelName] gets info for section with given label.
  //  see newSection in the findSections method for details
  this.sections = []; //new Array();
  // array of lines to append to the rotate log 
  this.logItems = new Array();
  this.findSections();
}


Todo.prototype.findSections = function()
{
  var ln = 0;
  // start with freeform section
  function newSection(cfg, line) {
    var cfg = {
      cfg: cfg, // section config
      startLine: line,
      endLine: null, // exclusive
      newLines: new Array(), // array of final lines to keep
      addedLines: new Array() // array of lines added from rotate
    };
    return cfg;
  }
  var currSection = newSection(null, 0);
  var numsec = 0;
  for ( ln = 0; ln < this.origLines.length; ++ln ) {
    var thisLine = this.origLines[ln];
    var endSection = false; // set to true if applicable
    var startSection = null; // set to section config obj if applicable
    if ( config.endSectionRegex.test(thisLine) ) {
      endSection = true;
    } else {
      // check whether it's the start of any section
      for ( var cs in config.sections ) {
        var secCfg = config.sections[cs];
        // regexes not working, so just a straight string match for now
        //if ( thisLine == "#" + secCfg.label ) {
        if ( trim(thisLine) === "#" + secCfg.label ) { //wim
          endSection = true;
          startSection = secCfg;
          break;
        }
        /*var rgx = new RegExp(config.labelPrefixRegex
                             + secCfg.label
                             + config.labelSuffixRegex);
        if ( rgx.test[thisLine] ) {
          startSection = secCfg;
          break;
        }*/
      }
    }
    
    if ( endSection ) {
      //echo("endSection = true for: " + thisLine);
      //logAddLine("endSection = true for: " + thisLine); //wim
      currSection.endLine = ln;
      var idx = currSection.cfg ? currSection.cfg.label : numsec;
      //echo(idx);
      //logAddLine('adding section: ' + idx);  //wim
      this.sections[idx] = currSection;
      ++numsec;
      currSection = newSection(startSection, ln);
    }
  } // end for each line
  
  if ( this.origLines.length-1 > currSection.startLine ) {
    currSection.endLine = this.origLines.length;
    var idx = currSection.cfg ? currSection.cfg.label : numsec;
    //echo("last is " + idx);
    //logAddLine('adding last section: ' + idx);  //wim
    this.sections[idx] = currSection;
    ++numsec;
  }
  
  // for debugging only
  //displaySections(this.sections);
  /*for ( var ss in this.sections ) {
    var sec = this.sections[ss];
    var lbl = sec.cfg ? sec.cfg.label : "*"; 
    var desc = lbl + "/" + sec.startLine + "/" + sec.endLine;
    logAddLine(desc);
  }*/
} // end findSections method

Todo.prototype.rotate = function(isWeekly)
{
  for ( var ss in this.sections ) {
    if (Object.prototype.hasOwnProperty.call(this.sections, ss))
    {
      var sec = this.sections[ss];
      // clear out completed to-publish
      // move completed items to log
      // rotate through sections
      
      // if no cfg, it's a freeform section, so don't monkey with it
      if ( !sec.cfg ) {
        //echo("no cfg for line starting at " + this.origLines[sec.startLine]);
        sec.newLines = this.origLines.slice(sec.startLine, sec.endLine);
        //echo("freeform sec: newlines#" + sec.newLines.length);
        continue;
      }
      
      for ( var ln = sec.startLine; ln < sec.endLine; ++ln ) {
        var thisLine = this.origLines[ln];
        var action = "keep";
        var lineType = checkTodoType(thisLine);
        switch ( lineType ) {
          case TodoType.notStarted:
            if ( sec.cfg.dailyRotate ) action = sec.cfg.dailyRotate;
            else if ( sec.cfg.weeklyRotate && isWeekly )
              action = sec.cfg.weeklyRotate;
            break;
          case TodoType.inProgress:
            if ( sec.cfg.dailyRotate ) action = sec.cfg.dailyRotate;
            else if ( sec.cfg.weeklyRotate && isWeekly )
              action = sec.cfg.weeklyRotate;
            break;
          case TodoType.complete:
            if ( sec.cfg.logCompleted ) action = "log";
            else action = "delete";
            break;
          default:
            action = "keep";
            break;
        }
        //echo("A" + action + " T" + lineType + ": " + thisLine);
        switch ( action ) {
          case "keep":
            sec.newLines.push(thisLine);
            break;
          case "log":
            this.logItems.push(thisLine);
            break;
          case "delete":
            // just don't put in newLines
            break;
          default:
            // action == rotate target
            this.sections[action].addedLines.push(thisLine);
        }
      } // end for each line
    }// end hasOwnProperty
  } // end for each section
} // end rotate method


/*=============================================================================================*//**
  Copy from Todo.prototype.rotate but only moves complete items.
*///================================================================================================
Todo.prototype.update = function()
{
  for ( var ss in this.sections ) {
    if (Object.prototype.hasOwnProperty.call(this.sections, ss))
    {
      var sec = this.sections[ss];
      // clear out completed to-publish
      // move completed items to log
      // rotate through sections
      
      // if no cfg, it's a freeform section, so don't monkey with it
      if ( !sec.cfg ) {
        //echo("no cfg for line starting at " + this.origLines[sec.startLine]);
        sec.newLines = this.origLines.slice(sec.startLine, sec.endLine);
        //echo("freeform sec: newlines#" + sec.newLines.length);
        continue;
      }
      
      for ( var ln = sec.startLine; ln < sec.endLine; ++ln ) {
        var thisLine = this.origLines[ln];
        var action = "keep";
        var lineType = checkTodoType(thisLine);
        switch ( lineType ) {
          case TodoType.complete:
            if ( sec.cfg.logCompleted ) action = "log";
            else action = "delete";
            break;
        }
        //echo("A" + action + " T" + lineType + ": " + thisLine);
        switch ( action ) {
          case "keep":
            sec.newLines.push(thisLine);
            break;
          case "log":
            this.logItems.push(thisLine);
            break;
          case "delete":
            // just don't put in newLines
            break;
          default:
            // action == rotate target
            this.sections[action].addedLines.push(thisLine);
        }
      } // end for each line
    }// end hasOwnProperty
  } // end for each section
} // end rotate method


Todo.prototype.dailyRotate = function()
{
  this.rotate(false);
}

Todo.prototype.weeklyRotate = function()
{
  this.rotate(true);
}

Todo.prototype.logFilePath = function()
{
  return trimExt(this.ed.fileName()) + config.todoLogExt;
}

function leftPad(text, size, ch)
{
  var result = text + '';
  ch = ch || " ";
  while (result.length < size) 
  {
    result = ch + result;
  }
  return result;
}


Todo.prototype.render = function()
{
  if ( config.doLogging && this.logItems.length > 0 ) {
    var logFilename = this.logFilePath();
    var logText = this.logItems.join(config.newline)
      + config.newline + config.newline;
    
    // http://msdn.microsoft.com/en-us/library/314cz14s%28VS.85%29.aspx
    var fso = new ActiveXObject("Scripting.FileSystemObject");
    
    // Get date todo file was modified, use that as when the log items  
    // were "completed".
    var statF = fso.GetFile(this.ed.fileName());
    var logDate = new Date(statF.DateLastModified);
    statF = null;
    
    var ForAppending = 8;
    
    var logF = fso.OpenTextFile(logFilename, ForAppending, true);
    // instead of logging current date, log last mod date of the file 
    //logF.WriteLine(logDate.toDateString());
    //@@wim: use YYYYMMDD format
    //echo (leftPad(9, 2, '0'));
    logF.WriteLine('' + logDate.getFullYear() + leftPad((logDate.getMonth() + 1), 2, '0') + leftPad(logDate.getDate(), 2, '0'));
    
    
    logF.Write(logText);
    logF.Close();
    logF = null;
    fso = null;
  }
  
  if ( config.modifyText ) {
    var newText = "";
    for ( var ss in this.sections ) {
      if (Object.prototype.hasOwnProperty.call(this.sections, ss))
      {      
        var sec = this.sections[ss];
        // remove trailing empty lines
        //logAddLine('section ' + ss);
        //logAddLine(sec.newLines.length);
        var lastIdx = rTrimIndex(sec.newLines, "");
        var lastIdx2 = rTrimIndex(sec.addedLines, "");
        if ( lastIdx < 0 ) lastIdx = sec.newLines.length;
        if ( lastIdx2 < 0 ) lastIdx2 = sec.addedLines.length;
        //logAddLine ("a.lastIdx=" + lastIdx + " lastIdx2 = " + lastIdx2);
        if ( lastIdx > 0 ) {
          if ( newText.length > 0 ) newText += config.newline;
          newText += sec.newLines.slice(0, lastIdx).join(config.newline);
        }
        if ( lastIdx2 > 0 ) {
          if ( newText.length > 0 ) newText += config.newline;
          newText += sec.addedLines.slice(0, lastIdx2).join(config.newline);
        }
        newText += config.newline;
        //logAddLine("b.new text for sec " + ss + " is:\n" + newText);
      }
    }
    //logAddLine("c.newText is \n" + newText);
    this.ed.text(newText);
  }
}

/////////////////////////// top-level commands //////////////////////////////

function update()
{
  try
  {
    var t = new Todo(getAndVerifyAndBackupCurrentFile());
    t.update();
    t.render();
  }
  catch(e)
  {
    echo("Error in update: " + e.message);
  }
}

function dailyRotate()
{
  try {
    var t = new Todo(getAndVerifyAndBackupCurrentFile());
    t.dailyRotate();
    t.render();
  } catch(e) {
    echo("Error in Daily Rotate: " + e.message);
  }
}

function weeklyRotate()
{
  try {
    var t = new Todo(getAndVerifyAndBackupCurrentFile());
    t.weeklyRotate();
    t.render();
  } catch(e) {
    echo("Error in Weekly Rotate: " + e);
  }
}


function viewLog()
{
  try {
    var t = new Todo(getAndVerifyAndBackupCurrentFile());
    var obj1 = newEditor();
    obj1.openFile(t.logFilePath());
  } catch(e) {
    if (e === "File is not a TODO file.")
    {
      try {
        viewProjectLog();
      } catch(e) {
        echo("Error in View Log: " + e);
      }
    }
    else
      echo("Error in View Log: " + e);
  }
}


function openScript() {
  var obj1 = newEditor();
  obj1.openFile(moduleFileName(module_name));
}


/*==================================================================================================
  Opens todo file for current project. 
  If no project file found, creates a new one in the projects root.
  If no project, creates a new unnamed one. 
==================================================================================================*/
function openTodo()
{
  //var editor = newEditor();
  //editor.openFile("E:\\Users\\Wim\\My Documents\\Logs\\WimsTodo.txt");
  var filename, fileSystemObject, editor; 
  
  filename = extractFilePath(projectFileName()) + "\\todo.txt"; 
  fileSystemObject = CreateObject("Scripting.FileSystemObject");
  editor = newEditor();
  if (fileSystemObject.FileExists(filename))
  {
    editor.openFile(filename);
  }
  else
  {
    editor.newFile(filename);
    editor.appendText("#!todofile!#\r\n");
    editor.appendText("\r\n");
    editor.appendText("#in queue\r\n");
    editor.appendText("\r\n");
    editor.appendText("#to publish\r\n");
    editor.appendText("\r\n");
    editor.appendText("#today\r\n");
    editor.appendText("\r\n");
    editor.appendText("#tomorrow\r\n");
    editor.appendText("\r\n");
    editor.appendText("#this week\r\n");
    editor.appendText("\r\n");
    editor.appendText("#next week\r\n");
    editor.appendText("\r\n");
    editor.appendText("#other week\r\n");
    editor.appendText("\r\n");
    editor.appendText("#eventually\r\n");
    editor.appendText("\r\n");
  }
}


function viewProjectLog()
{
  var filename, fileSystemObject, editor;
  filename = extractFilePath(projectFileName()) + "\\todo.log.txt";
  
  fileSystemObject = CreateObject("Scripting.FileSystemObject");
  if (!fileSystemObject.FileExists(filename))
    throw "No project log file found.";
  editor = newEditor();
  editor.openFile(filename);
}



function Init(){
  var menuName = "Todo";
  addMenuItem("Open Todo"         , menuName, "openTodo");
  addMenuItem("-"                 , menuName, "");
  addMenuItem("Update"            , menuName, "update");
  addMenuItem("Daily Rotate"      , menuName, "dailyRotate");
  addMenuItem("Weekly Rotate"     , menuName, "weeklyRotate");
  addMenuItem("View Log"          , menuName, "viewLog");
  addMenuItem("-"                 , menuName, "");
  addMenuItem("Edit Script File"  , menuName, "openScript");
}
